From d3f784b6b3096ec03305565820a890379fb03764 Mon Sep 17 00:00:00 2001 From: "Karl O. Pinc" Date: Sat, 16 Mar 2024 16:31:30 -0500 Subject: [PATCH] Account for warnings when testing; add new test fixtures --- src/pgwui_develop/testing.py | 127 +++++++++++++++++++++++++++++------ tests/test_pgwui.py | 21 +++--- tests/test_testing.py | 118 +++++++++++++++++++++++++++++++- tests/tested_module.py | 27 ++++++++ tox.ini | 2 + 5 files changed, 263 insertions(+), 32 deletions(-) create mode 100644 tests/tested_module.py diff --git a/src/pgwui_develop/testing.py b/src/pgwui_develop/testing.py index 8c81abe..bada279 100644 --- a/src/pgwui_develop/testing.py +++ b/src/pgwui_develop/testing.py @@ -20,58 +20,147 @@ # Karl O. Pinc -from pytest import fixture from unittest import mock +import functools +import sys +import warnings +import pytest + + +# Utility decorator + +# Pyramid 2.0 added deprecation warnings to pyramid.request.Request. +# These express when unittest.mock accesses the class' attributes +# to construct a mock, when the mock_request_blank fixture is used +# and a mock made. Ignore all warnings triggered by unittest.mock. +def ignore_deprecation_warnings(ignore): + def identity(func): + return func + + def wrapper(func): + @functools.wraps(func) + def wrapped(*args, **kwargs): + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", category=DeprecationWarning, + module='unittest.mock') + return func(*args, **kwargs) + return wrapped + + if ignore: + return wrapper + return identity # Mock support -def make_mock_fixture(module, method, autouse=False, wraps=None): +def make_mock_fixture(module, method, + autouse=False, wraps=None, ignore_deprecation=True): '''Returns a pytest fixture that mocks a module's method or a class's class method. "module" is a module or a class, but method is a string. ''' - @fixture(autouse=autouse) - def fix(monkeypatch): - mocked = mock.Mock( + @ignore_deprecation_warnings(ignore_deprecation) + def make_mock(): + return mock.Mock( spec=getattr(module, method), name=method, wraps=wraps) + + @pytest.fixture(autouse=autouse) + def fix(monkeypatch): + mocked = make_mock() monkeypatch.setattr(module, method, mocked) return mocked return fix -def make_magicmock_fixture(module, method, autouse=False, autospec=False): +def make_magicmock_fixture( + module, method, + autouse=False, autospec=False, ignore_deprecation=True): '''Returns a pytest fixture that magic mocks a module's method or a class's class method. "module" is a module or a class, but method is a string. ''' - @fixture(autouse=autouse) - def fix(monkeypatch): + @ignore_deprecation_warnings(ignore_deprecation) + def make_mocked(): if autospec: - mocked = mock.create_autospec( + return mock.create_autospec( getattr(module, method), spec_set=True) - else: - mocked = mock.MagicMock( - spec=getattr(module, method), name=method) + return mock.MagicMock( + spec=getattr(module, method), name=method) + + @pytest.fixture(autouse=autouse) + def fix(monkeypatch): + mocked = make_mocked() monkeypatch.setattr(module, method, mocked) return mocked return fix +def function_mock_fixture(method, ignore_deprecation=True): + '''Returns a pytest fixture that mocks a function. + "method" is the actual function. + The primary purpose is to monkeypatch modules + that are needed by python's mechanics. + ''' + name = method.__name__ + module = sys.modules[method.__module__] + + @ignore_deprecation_warnings(ignore_deprecation) + def make_mocked(): + return mock.Mock(spec=module, name=name) + + @pytest.fixture + def fix(monkeypatch): + def run(): + mocked = make_mocked() + func = getattr(mocked, name) + monkeypatch.setattr(module, name, func) + return func + return run + return fix + + +def instance_mock_fixture(method, ignore_deprecation=True): + '''Returns a pytest fixture that mocks a instance method. + "method" is the actual instance or class method. + The primary purpose is to monkeypatch classes + that are needed by python's mechanics. + ''' + name = method.__name__ + cls = method.__self__ + + @ignore_deprecation_warnings(ignore_deprecation) + def make_mocked(): + return mock.Mock(spec=cls, name=name) + + @pytest.fixture + def fix(monkeypatch): + def run(): + mocked = make_mocked() + func = getattr(mocked, name) + monkeypatch.setattr(cls, name, func) + return func + return run + return fix + + def late_instance_mock_fixture(method, ignore_deprecation=True): '''Returns a pytest fixture that mocks a instance method. "method" is the name of the instance or class method. The function returned by the fixture takes the class instance to be monkeypatched. - The fixture is called by the test function with the class instance - that's to be monkeypatched and the mock is returned for the - test function to configure/etc. + Useful to monkeypatch classes produced by fixtures. ''' - @fixture + @ignore_deprecation_warnings(ignore_deprecation) + def make_mocked(cls): + return mock.Mock(spec=cls, name=method) + + @pytest.fixture def fix(monkeypatch): def run(cls): - mocked = mock.Mock(spec=getattr(cls, method), name=method) - monkeypatch.setattr(cls, method, mocked) - return mocked + mocked = make_mocked(cls) + func = getattr(mocked, method) + monkeypatch.setattr(cls, method, func) + return func return run return fix diff --git a/tests/test_pgwui.py b/tests/test_pgwui.py index 1807efe..b4f2edd 100644 --- a/tests/test_pgwui.py +++ b/tests/test_pgwui.py @@ -1,4 +1,5 @@ -# Copyright (C) 2020, 2021 The Meme Factory, Inc. http://www.karlpinc.com/ +# Copyright (C) 2020, 2021, 2024 The Meme Factory, Inc. +# http://www.karlpinc.com/ # This file is part of PGWUI_Develop. # @@ -35,12 +36,12 @@ mock_text_error_template = testing.make_magicmock_fixture( pgwui.mako.exceptions, 'text_error_template') MockTemporaryDirectory = testing.make_magicmock_fixture( pgwui.tempfile, 'TemporaryDirectory') -mock_set_extraction_path = testing.instance_method_mock_fixture( - 'set_extraction_path') -mock_resource_filename = testing.instance_method_mock_fixture( - 'resource_filename') -mock_cleanup_resources = testing.instance_method_mock_fixture( - 'cleanup_resources') +mock_set_extraction_path = testing.function_mock_fixture( + pgwui.pkg_resources.set_extraction_path) +mock_resource_filename = testing.function_mock_fixture( + pgwui.pkg_resources.resource_filename) +mock_cleanup_resources = testing.function_mock_fixture( + pgwui.pkg_resources.cleanup_resources) mock_scandir = testing.make_magicmock_fixture( pgwui.os, 'scandir') @@ -376,9 +377,9 @@ def test_deliver_target( mock_cleanup_resources): '''All the mocks are called ''' - mocked_set_extraction_path = mock_set_extraction_path(pgwui.pkg_resources) - mocked_resource_filename = mock_resource_filename(pgwui.pkg_resources) - mocked_cleanup_resources = mock_cleanup_resources(pgwui.pkg_resources) + mocked_set_extraction_path = mock_set_extraction_path() + mocked_resource_filename = mock_resource_filename() + mocked_cleanup_resources = mock_cleanup_resources() pgwui.deliver_target(None, None) diff --git a/tests/test_testing.py b/tests/test_testing.py index a724583..7680763 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -24,14 +24,68 @@ import pytest - import sys +import tested_module from pgwui_develop import testing # Test functions +# +# ignore_depreciation_warnings() +# + + +@pytest.mark.parametrize( + ('ignore', 'warn_cnt'), + [pytest.param( + 'True', 0, + marks=pytest.mark.xfail( + reason="For reasons unknown, pytester reports 1 warnings")), + ('False', 1)]) +@pytest.mark.integrationtest +@pytest.mark.unittest +def test_ignore_deprecation_warnings_ignore(pytester, ignore, warn_cnt): + '''The expected number of deprecation warnings are raised when warnings + are ignored, or not + ''' + + pytester.makepyfile( + f""" + import pytest + import warnings + from unittest import mock + from pgwui_develop import testing + + # Class which raises a deprecation warning + class WithDeprecation(): + '''Test class that raises a DeprecationWarning when mocked + ''' + def __getattribute__(self, name): + if name == 'oldmethod': + warnings.warn('oldmethod is deprecated', + category=DeprecationWarning) + return super().__getattribute__(name) + + def oldmethod(self): + return "I am old, so old." + + mocked_oldmethod = testing.make_mock_fixture( + WithDeprecation, 'oldmethod', + ignore_deprecation={ignore}) + + def test_ignore_deprecation_warnings_ignore(mocked_oldmethod): + expected = 'something' + mocked_oldmethod.return_value = expected + assert WithDeprecation().oldmethod() == expected + """) + + result = pytester.runpytest() + + result.assert_outcomes(passed=1, warnings=warn_cnt) + + # # make_mock_fixture() # @@ -94,9 +148,38 @@ def test_make_magicmock_fixture_autospec(magic_mocked_autospecced_func): # -# instance_method_mock_fixture() +# function_mock_fixture() # +f_mocked_method = testing.function_mock_fixture(tested_module.method_to_mock) + + +@pytest.mark.unittest +@pytest.mark.integrationtest +def test_function_mock_fixture(f_mocked_method): + # The mock of the instance method works + + test_value = 'mocked value' + f_mocked_method().return_value = test_value + + result = tested_module.method_to_mock() + + assert result == test_value + + +@pytest.mark.unittest +@pytest.mark.integrationtest +def test_function_mock_fixture_unmocked(): + # The test function works after the mocking + + result = tested_module.method_to_mock() + + assert result == tested_module.normal_return_value + + +# +# Setup for mocking instance methods +# normal_return_value = 'not mocked' @@ -107,7 +190,36 @@ class TestClass(): return normal_return_value -mocked_method = testing.instance_method_mock_fixture('method_to_mock') +test_instance = TestClass() + + +# +# instance_mock_fixture() +# +i_mocked_method = testing.instance_mock_fixture(test_instance.method_to_mock) + + +@pytest.mark.unittest +@pytest.mark.integrationtest +def test_instance_mock_fixture(i_mocked_method): + # The mock of the instance method works + + test_value = 'mocked value' + i_mocked_method().return_value = test_value + + result = test_instance.method_to_mock() + + assert result == test_value + + +@pytest.mark.unittest +@pytest.mark.integrationtest +def test_instance_mock_fixture_unmocked(): + # The test function works after the mocking + + result = test_instance.method_to_mock() + + assert result == normal_return_value # diff --git a/tests/tested_module.py b/tests/tested_module.py new file mode 100644 index 0000000..59004ab --- /dev/null +++ b/tests/tested_module.py @@ -0,0 +1,27 @@ +# Copyright (C) 2024 The Meme Factory, Inc. +# http://www.karlpinc.com/ + +# This file is part of PGWUI_Develop. +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation, either version 3 of +# the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# + +# Karl O. Pinc + +normal_return_value = 'not mocked' + + +def method_to_mock(): + return normal_return_value diff --git a/tox.ini b/tox.ini index aca3ac0..eb2c654 100644 --- a/tox.ini +++ b/tox.ini @@ -49,3 +49,5 @@ addopts = --cov --cov-append markers = unittest integrationtest +pytest_plugins = + pytester -- 2.34.1